/***************************************************************************
 *            testscenes.cc
 *
 *  Copyright  2008  Sebastian Mach
 *  seb@greenhybrid.net
 ****************************************************************************/

/*
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; version 3 of the License, or (at your
 *  option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include <iostream>
#include <vector>

#include <SDL/SDL.h>

#include <picogen/picogen.h>
#include <picogen/graphics/objects/Instance.h>
#include <picogen/graphics/objects/templates/TriBIH.h>



using picogen::real;
using picogen::geometrics::Vector3d;
using picogen::geometrics::Ray;
using picogen::graphics::color::Color;
using picogen::graphics::structs::intersection_t;

using picogen::geometrics::Transformation;

using picogen::graphics::objects::templates::TriBIH;



struct t_triangle {
    private:
        Vector3d t[3];
    public:
        t_triangle (Vector3d a, Vector3d b, Vector3d c) {
            t[0] = a;
            t[1] = b;
            t[2] = c;
        }
        t_triangle() {
        }

        const Vector3d & operator [] ( int i ) const {
            return t[i];
        }
};
typedef TriBIH<t_triangle> genBIH;
static genBIH myGenBIH;



class ConstantShader : public picogen::graphics::material::abstract::IShader {
        picogen::graphics::color::Color color;
    public:
        virtual ~ConstantShader() {};
        ConstantShader (picogen::graphics::color::Color color) : color (color) {}
        virtual void shade (
            picogen::graphics::color::Color &color,
            const picogen::geometrics::Vector3d &normal,
            const picogen::geometrics::Vector3d &position
        ) const {
            color = this->color;
        }
};
static const ConstantShader  red (picogen::graphics::color::Color (1.0, 0.3, 0.3));
static const ConstantShader  green (picogen::graphics::color::Color (0.3, 1.0, 0.3));
static const ConstantShader  blue (picogen::graphics::color::Color (0.3, 0.3, 1.0));
static const ConstantShader  white (picogen::graphics::color::Color (1.0, 1.0, 1.0));


template<class t_surface>
void
draw (
    SDL_Surface *p_target,
    const t_surface &surface,
    float scale,
    float exp_tone,
    float saturation
) {
    if (SDL_MUSTLOCK (p_target) && SDL_LockSurface (p_target) <0)
        return;
    int x,y;
    for (y=0; y<p_target->h; y++) {
        /// \todo FIX we are currently assuming a 32bit SDL buffer
        /// \todo get rid of pointer arithmetic
        Uint32 *bufp   = (Uint32*) p_target->pixels + y* (p_target->pitch>>2);
        for (x=0; x<p_target->w; x++) {
            /*float d = 1.0 / source->density;
            float r = source->color[0]*scale*d;
            float g = source->color[1]*scale*d;
            float b = source->color[2]*scale*d;*/
            real accu_r=real (0), accu_g=real (0), accu_b=real (0);
            for (register int u=0; u<2; ++u) for (register int v=0; v<2; ++v) {
                    real r,g,b;
                    ( (Color) surface (x*2+u,y*2+v)).to_rgb (r, g, b);
                    //((xrt::base_types::color)surface((unsigned)x,(unsigned)y)).to_rgb( r, g, b );
                    // tone map

                    /*
                    const float brightness = 0.299*r + 0.584*g + 0.144*b;
                    const float e = 1-exp(-brightness * exp_tone );
                    r = (r/brightness) * e;
                    g = (g/brightness) * e;
                    b = (b/brightness) * e;
                    //*/

                    // scale linearly
                    r *= scale;
                    g *= scale;
                    b *= scale;
                    // saturate color
                    const float sbrightness = 0.299*r + 0.584*g + 0.144*b;
                    r = saturation*r + (1-saturation) *sbrightness;
                    g = saturation*g + (1-saturation) *sbrightness;
                    b = saturation*b + (1-saturation) *sbrightness;
                    // final saturation
                    accu_r += r<0 ? 0 : r>1 ? 1 : r;
                    accu_g += g<0 ? 0 : g>1 ? 1 : g;
                    accu_b += b<0 ? 0 : b>1 ? 1 : b;
                }
            accu_r *= 0.25;
            accu_g *= 0.25;
            accu_b *= 0.25;

            * (bufp++) =
                SDL_MapRGB (p_target->format,
                            (unsigned char) (255.0*accu_r),
                            (unsigned char) (255.0*accu_g),
                            (unsigned char) (255.0*accu_b)
                           );
        }
    }


    if (SDL_MUSTLOCK (p_target))
        SDL_UnlockSurface (p_target);
    SDL_Flip (p_target);
}





class TestScene {
    public:
        virtual std::string getName() const = 0;
        virtual void initRenderer (int width, int height) = 0;
        virtual void flip (SDL_Surface *screen, float scale, float saturation) = 0;
        virtual void begin() = 0;
        virtual bool renderMore (int numPixels) = 0;  // i wanted to call it continue, but then continue is obviously a reserved word :P
        virtual void end() = 0;
};


class PureCornell : public TestScene {
    protected:
        picogen::graphics::integrators::screen::XYIterator renderer;
        int width, height;
        picogen::common::LinearList list;
        picogen::common::Preetham   preetham;
        picogen::common::AABox      box, innerbox;
        picogen::graphics::objects::Instance   box1;

    public:

        explicit PureCornell() : box1 (&innerbox) {
            box.enableOutside (false);
        }
        virtual std::string getName() const {
            return std::string ("pure-cornell");
        }

        virtual void initRenderer (int width, int height) {
            using namespace picogen;

            this->width  = width;
            this->height = height;
            // setup screen and camera
            ::picogen::graphics::cameras::FromPointToRect *fptr = new ::picogen::graphics::cameras::FromPointToRect;
            fptr->defineCamera ( (real) width/ (real) height, 1.0, 0.85);
            renderer.setCamera (fptr);
            //renderer.surface().reset (width*2, height*2);
            ::picogen::graphics::film::SimpleColorFilm *film = new ::picogen::graphics::film::SimpleColorFilm (width*2, height*2);
            renderer.setFilm (film);

            // setup camera transform
            renderer.transformation() =
                Transformation().setToRotationX (3.14159*0.0) *
                Transformation().setToRotationY (3.14159*0.0) *
                Transformation().setToTranslation (Vector3d (0.0,0.0,-1.5) *1.0)
                ;

            // setup and recognize sky
            preetham.setTurbidity (2.3);
            preetham.setSunSolidAngleFactor (1.0);
            const real L = 0.25;
            preetham.setColorFilter (Color (1.0,1.0,1.0) *1.0*L);
            preetham.setSunColor (Color (0.95,0.95,1.0) *3000.0*L);
            preetham.setSunDirection (Vector3d (-1.0,1.8,-2.3).normal());
            preetham.enableFogHack (0, 0.00082*0.05, 50000);
            preetham.invalidate();
            renderer.setSky (&preetham);

            // setup boxen
            using picogen::geometrics::Transformation;
            innerbox.setShader (&white);
            box1.setTransform (
                Transformation().setToScaling (1.0/0.5)
                //* Transformation().setToRotationY(0.3)
                * Transformation().setToTranslation (Vector3d (0.0,1.0,0.0))
            );

            // recognize scene
            list.insert (&box);
            //list.Insert( &box1 );

            // invalidate and recognize object-list
            list.invalidate();
            renderer.setIntersectable (&list);

            ::picogen::graphics::integrators::surface::ISurfaceIntegrator *surfaceIntegrator = new ::picogen::graphics::integrators::surface::Path();
            //surfaceIntegrator->setIntersectable (&list);
            renderer.setSurfaceIntegrator (surfaceIntegrator);
        }

        virtual void flip (SDL_Surface *screen, float scale, float saturation) {
            ::SDL_Flip (screen);
            ::draw (screen,*renderer.getFilm(), scale, 1.0, saturation);//scale, exp_tone, saturation);
        }

        virtual void begin() {
            renderer.beginRender();
        }

        virtual bool renderMore (int numPixels) {
            renderer.setNumPixelsPerContinue (numPixels);
            bool c = renderer.renderMore();
            if (c) {
                renderer.oneMoreRun();
            }
            return c;
        }

        virtual void end() {
            /// \todo add deletes!!!
        }
};

class CornellOpenSky : public PureCornell {
    public:
        virtual std::string getName() const {
            return std::string ("cornell-open-sky");
        }
        virtual void initRenderer (int width, int height) {
            PureCornell::initRenderer (width, height);
            box.enableFace (box.y_positive, false);
        }
};


#include <picogen/graphics/objects/CloudsBouthors.h>

class Clouds : public TestScene {
    private:
        picogen::graphics::integrators::screen::XYIterator renderer;
        int width, height;
        picogen::graphics::objects::LinearList list;
        picogen::graphics::objects::Preetham   preetham;
        picogen::graphics::objects::ZBNCubemap cubemap, cloudcubemap;
        picogen::graphics::objects::CloudsBouthors clouds;

    public:
        explicit Clouds() {
        }

        virtual std::string getName() const {
            return std::string ("clouds");
        }

        virtual void initRenderer (int width, int height) {
            using namespace picogen;

            this->width  = width;
            this->height = height;
            // setup screen and camera
            ::picogen::graphics::cameras::FromPointToRect_Cylinder *fptr = new ::picogen::graphics::cameras::FromPointToRect_Cylinder;
            fptr->defineCamera ( (real) width/ (real) height, 1.0, 0.85);
            renderer.setCamera (fptr);
            ::picogen::graphics::film::SimpleColorFilm *film = new ::picogen::graphics::film::SimpleColorFilm (width*2, height*2);
            renderer.setFilm (film);

            // setup camera transform
            renderer.transformation() =
                Transformation().setToRotationX (3.14159*-0.0) *
                Transformation().setToRotationY (3.14159*0.0) *
                Transformation().setToTranslation (Vector3d (0.0,0.0,0.0))
                ;

            // setup and recognize sky
            preetham.setTurbidity (2.7);
            preetham.setSunSolidAngleFactor (1.0);
            const real L = 0.25;
            preetham.setColorFilter (Color (1.0,1.0,1.0) *1.0*L);
            preetham.setSunColor (Color (0.95,0.95,1.0) *3000.0*L);
            preetham.setSunDirection (Vector3d (0.0,1.0,-1.0).normal());
            preetham.enableFogHack (0, 0.00082*0.05, 50000000);
            preetham.invalidate();
            renderer.setSky (&preetham);


            {
                int cbsize = 128;
                cubemap.getSurface (cubemap.x_negative).resize (cbsize,cbsize);
                cubemap.getSurface (cubemap.x_positive).resize (cbsize,cbsize);
                cubemap.getSurface (cubemap.y_negative).resize (cbsize,cbsize);
                cubemap.getSurface (cubemap.y_positive).resize (cbsize,cbsize);
                cubemap.getSurface (cubemap.z_negative).resize (cbsize,cbsize);
                cubemap.getSurface (cubemap.z_positive).resize (cbsize,cbsize);
            }
            {
                int cbsize = 256;
                cloudcubemap.getSurface (cubemap.x_negative).resize (cbsize,cbsize);
                cloudcubemap.getSurface (cubemap.x_positive).resize (cbsize,cbsize);
                cloudcubemap.getSurface (cubemap.y_negative).resize (cbsize,cbsize);
                cloudcubemap.getSurface (cubemap.y_positive).resize (cbsize,cbsize);
                cloudcubemap.getSurface (cubemap.z_negative).resize (cbsize,cbsize);
                cloudcubemap.getSurface (cubemap.z_positive).resize (cbsize,cbsize);
            }
            /* clouds */ {
                graphics::objects::SimpleHeightField heightField;

                using namespace picogen::misc::functions;
                uv_to_scalar::PerlinNoise function4;
                function4.setPersistence (0.85);
                function4.setBaseFrequency (1);
                function4.setOctaves (5);
                function4.setDomainScale (3.0);
                function4.setRangeScale (0.0125);
                function4.setMode (function4.normal);
                function4.setMod (function4.mod_normal);
                function4.setEvalScale (1.9);
                function4.seed (5);
                uv_to_scalar::Power moon_multifractal (&function4, 1.0);

                ::picogen::graphics::material::brdf::Lambertian lambertian;
                heightField.setBRDF (&lambertian);
                heightField.setShader (&white);
                heightField.setBox (Vector3d (-10000,0,-10000), Vector3d (10000,400.0,10000));
                heightField.init (1024, &moon_multifractal, 0.1, false);

                cloudcubemap.render (Vector3d (0,400+0*-1300,0), &heightField);
                clouds.setIntersectable (&cloudcubemap);
                clouds.setSky (&preetham);
                list.insert (&clouds);
            }

            /* terrain */
            if (0) {
                graphics::objects::SimpleHeightField heightField;

                using namespace picogen::misc::functions;
                uv_to_scalar::PerlinNoise function4;
                function4.setPersistence (0.95);
                function4.setBaseFrequency (2);
                function4.setOctaves (8);
                function4.setDomainScale (3.0);
                function4.setRangeScale (0.0125);
                function4.setMode (function4.ridged);
                function4.setMod (function4.mod_heterofractal);
                function4.setEvalScale (1.9);
                function4.seed (5);
                uv_to_scalar::Power moon_multifractal (&function4, 0.4);

                ::picogen::graphics::material::brdf::Lambertian lambertian;
                heightField.setBRDF (&lambertian);
                heightField.setShader (&white);
                heightField.setBox (Vector3d (-10000,-3000,-10000), Vector3d (10000,0.0,10000));
                heightField.init (256, &moon_multifractal, 0.1, false);

                cubemap.render (Vector3d (0,0,0), &heightField);
                list.insert (&cubemap);
            }

            // invalidate and recognize object-list
            list.invalidate();
            renderer.setIntersectable (&list);
            ::picogen::graphics::integrators::surface::ISurfaceIntegrator *surfaceIntegrator = new ::picogen::graphics::integrators::surface::Path();
            //surfaceIntegrator->setIntersectable (&list);
            renderer.setSurfaceIntegrator (surfaceIntegrator);
        }

        virtual void flip (SDL_Surface *screen, float scale, float saturation) {
            ::SDL_Flip (screen);
            ::draw (screen,*renderer.getFilm(), scale, 1.0, saturation);//scale, exp_tone, saturation);
        }

        virtual void begin() {
            renderer.beginRender();
        }
        virtual bool renderMore (int numPixels) {
            renderer.setNumPixelsPerContinue (numPixels);
            bool c = renderer.renderMore();
            if (c) {
                renderer.oneMoreRun();
            }
            return c;
        }
        virtual void end() {
            /// \todo add deletes!!!
        }
};




static int loop (SDL_Surface *screen, TestScene *scene, int width, int height) {
    //scene->flip( screen );
    // + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    // prepare and run loop
    // + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    unsigned int pixelsPerContinue = (width*height*4) /16;
    unsigned int runCount = 1;
    //clock_t startTime = clock();
    bool done = false;
    bool queryingForReallyQuit = false;
    unsigned int numManDumbs = 0;
    float scale = 1;
    float exp_tone = 1.0;
    float saturation = 1.0;

    srand (time (0));


    //renderer.SetNumPixelsPerContinue( pixelsPerContinue );//*(HEIGHT/1) );
    //renderer.BeginRender();
    scene->begin();
    while (!done) {
        SDL_Event event;
        while (SDL_PollEvent (&event)) {
            if (event.type == SDL_QUIT) {
                done = true;
            }
            if (event.type == SDL_KEYDOWN) {
                if (queryingForReallyQuit) {
                    if (event.key.keysym.sym == SDLK_y) {
                        done = true;
                        queryingForReallyQuit = false;
                    } else if (event.key.keysym.sym == SDLK_n) {
                        done = false;
                        queryingForReallyQuit = false;
                    }
                } else if (event.key.keysym.sym == SDLK_ESCAPE) {
                    queryingForReallyQuit = 1;
                }
                if (event.key.keysym.sym == SDLK_d) {
                    char s[256];
                    sprintf (s, "screendump.%.4d.bmp", numManDumbs++);
                    SDL_SaveBMP (screen, s);
                }
                if (event.key.keysym.sym == SDLK_y) {
                    pixelsPerContinue-=100*width; /*renderer.SetNumPixelsPerContinue( pixelsPerContinue );*/
                }
                if (event.key.keysym.sym == SDLK_x) {
                    pixelsPerContinue-=10*width; /*renderer.SetNumPixelsPerContinue( pixelsPerContinue );*/
                }
                if (event.key.keysym.sym == SDLK_c) {
                    pixelsPerContinue-=width; /*renderer.SetNumPixelsPerContinue( pixelsPerContinue );*/
                }
                if (event.key.keysym.sym == SDLK_v) {
                    pixelsPerContinue+=width; /*renderer.SetNumPixelsPerContinue( pixelsPerContinue );*/
                }
                if (event.key.keysym.sym == SDLK_b) {
                    pixelsPerContinue+=10*width; /*renderer.SetNumPixelsPerContinue( pixelsPerContinue );*/
                }
                if (event.key.keysym.sym == SDLK_n) {
                    pixelsPerContinue+=100*width; /*renderer.SetNumPixelsPerContinue( pixelsPerContinue );*/
                }

                if (event.key.keysym.sym == SDLK_5) {
                    scale = 1.0;
                }
                if (event.key.keysym.sym == SDLK_6) {
                    scale += 0.01;
                }
                if (event.key.keysym.sym == SDLK_7) {
                    scale += 0.1;
                }
                if (event.key.keysym.sym == SDLK_8) {
                    scale += 1.0;
                }
                if (event.key.keysym.sym == SDLK_9) {
                    scale += 10.0;
                }
                if (event.key.keysym.sym == SDLK_4) {
                    scale -= 0.01;
                }
                if (event.key.keysym.sym == SDLK_3) {
                    scale -= 0.1;
                }
                if (event.key.keysym.sym == SDLK_2) {
                    scale -= 1.0;
                }
                if (event.key.keysym.sym == SDLK_1) {
                    scale -= 10.0;
                }

                if (event.key.keysym.sym == SDLK_t) {
                    exp_tone = 1.0;
                }
                if (event.key.keysym.sym == SDLK_z) {
                    exp_tone += 0.01;
                }
                if (event.key.keysym.sym == SDLK_u) {
                    exp_tone += 0.1;
                }
                if (event.key.keysym.sym == SDLK_i) {
                    exp_tone += 1.0;
                }
                if (event.key.keysym.sym == SDLK_o) {
                    exp_tone += 10.0;
                }
                if (event.key.keysym.sym == SDLK_r) {
                    exp_tone -= 0.01;
                }
                if (event.key.keysym.sym == SDLK_e) {
                    exp_tone -= 0.1;
                }
                if (event.key.keysym.sym == SDLK_w) {
                    exp_tone -= 1.0;
                }
                if (event.key.keysym.sym == SDLK_q) {
                    exp_tone -= 10.0;
                }

                if (event.key.keysym.sym == SDLK_g) {
                    saturation -= 0.1;
                }
                if (event.key.keysym.sym == SDLK_h) {
                    saturation -= 0.01;
                }
                if (event.key.keysym.sym == SDLK_j) {
                    saturation = 1.0;
                }
                if (event.key.keysym.sym == SDLK_k) {
                    saturation += 0.01;
                }
                if (event.key.keysym.sym == SDLK_l) {
                    saturation += 0.1;
                }

            }
        }

        bool cont = scene->renderMore (pixelsPerContinue);
        if (cont) {
            runCount++;
            //
            //renderer.OneMoreRun();
            //if( 0 == (runCount%3) )
            //    renderer.surface().saveBinaryToFile( checkpointFile );
        } else if (queryingForReallyQuit) {
            SDL_WM_SetCaption ("Really Quit? (Y/N) ", "Really Quit? (Y/N) ");
        } else {
            char s[256];
            //float t = (static_cast<float>(clock()-startTime))/static_cast<float>(CLOCKS_PER_SEC);
            //sprintf( s, "%uspp:%ux%u/%.2fsec/color{scale%.2f", runCount, WIDTH, HEIGHT , t, scale );
            sprintf (s, "%uspp;color.{*%.2f,t%.2f,s%.2f};refresh{%.2fl}", runCount, scale, exp_tone, saturation, (float) pixelsPerContinue/ (float) width);
            fprintf (stderr, "\rrendering pass %u (%s)", 1+runCount, s);
            SDL_WM_SetCaption (s, "...");
        }

        //scene->flip( screen );
        scene->flip (screen, scale, saturation);
        //draw(screen,renderer.surface(),scale, exp_tone, saturation);
        /*unsigned int numSamples;
        for( numSamples=0; numSamples<10000; numSamples ++ ){
            unsigned int u = rand() % surf.width();
            unsigned int v = rand() % surf.height();
            if( (rand()%500000) > u*u+v*v )
                surf(u,v) += base_types::color(1,1,1);
        }*/
    }
    scene->end();
    return 0;
}

static int grind (int width, int height, TestScene *scene) {
    using namespace std;
    scene->initRenderer (width, height);

    if (SDL_Init (SDL_INIT_VIDEO) < 0) {
        cerr << "Unable to init SDL: " << SDL_GetError() << endl;
        return 1;
    }
    atexit (SDL_Quit);
    SDL_Surface *screen = SDL_SetVideoMode (width,height,32,SDL_HWSURFACE|SDL_DOUBLEBUF);
    if (0 == screen) {
        cerr << "Unable to set video-mode: " << SDL_GetError() << endl;
        return 1;
    }

    //scene->flip();
    return loop (screen, scene, width, height);  /// \todo check if SQL cleans up surface resource
}



int main_testscenes (int argc, char *argv[]) {
    using namespace std;
    static PureCornell pureCornell;
    static CornellOpenSky cornellOpenSky;
    static Clouds clouds;
    vector<TestScene*> scenes;

    #ifndef PICOGEN_RELEASE
    scenes.push_back (&clouds);
    scenes.push_back (&pureCornell);
    #endif
    scenes.push_back (&cornellOpenSky);

    TestScene* grindScene = 0;

    while (0 == grindScene) {
        cout << "the following test-scenes are available:\n\n  ";
        int i=0;
        for (vector<TestScene*>::const_iterator it = scenes.begin(); it != scenes.end(); ++it) {
            if (it != scenes.begin()) cout << ", ";
            cout << (*it)->getName() << " [" << i << "]";
            ++i;
        }
        cout << "\n" << endl;
        cout << "which one do you want to grind (enter number)? ";
        unsigned int num;
        cin >> num;
        if (num < scenes.size()) {
            grindScene = scenes[num];
            break;
        }
    }
    cout << "you have made your choice: " << grindScene->getName() << endl;

    int width, height;
    cout << "enter width (0 for default 320): ";
    cin >> width;
    cout << "enter height (0 for default 320): ";
    cin >> height;
    if (width == 0) width = 320*2;
    if (height == 0) height = 80*2;  //320;

    cout << "your resolution of choice is: " << width << "x" << height << endl;
    cout << "commencing mission" << endl;
    return grind (width, height, grindScene);
}
